# coding=utf-8

"""
Author: zhangzheng
Description: 
Version: 0.0.1
LastUpdateDate: 
UpadteURL: 
LOG: 
"""

import requests
from requests_toolbelt import MultipartEncoder
import copy
import json
from typing import Union
import re
import time

from app.cores.dictionaries import CONTENT_TYPE
from app.cores.case.http.request_util import handle_params, handle_data_for_content_type, handle_file_upload, \
    save_binary_to_file, handle_data_form_json_str, handle_data_from_params


class HTTPRequest:

    _method = None

    def __init__(self, url, **kwargs):
        """
        :param url: URL for the new :class:`Request` object.
        :param params: (optional) Dictionary, list of tuples or bytes to send
            in the query string for the :class:`Request`.
        :param data: (optional) Dictionary, list of tuples, bytes, or file-like
            object to send in the body of the :class:`Request`.
        :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
        :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
        :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
        :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
            ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
            or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
            defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
            to add for the file.
        :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
        :param timeout: (optional) How many seconds to wait for the server to send data
            before giving up, as a float, or a :ref:`(connect timeout, read
            timeout) <timeouts>` tuple.
        :type timeout: float or tuple
        :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
        :type allow_redirects: bool
        :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
        :param verify: (optional) Either a boolean, in which case it controls whether we verify
                the server's TLS certificate, or a string, in which case it must be a path
                to a CA bundle to use. Defaults to ``True``.
        :param stream: (optional) if ``False``, the response content will be immediately downloaded.
        :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
        """
        self.url = url
        # self.params = kwargs.get('params')
        # self.data = kwargs.get('data')
        # self.json = kwargs.get('json')
        # self.header = kwargs.get('header')
        self.kwargs = kwargs
        # 应答
        self.response = None  # type: requests.Response
        # deepcopy from self.response
        self.response_headers = None  # type: dict
        self.response_body = None  # type: Union[str, dict, list] # 可能是字典/列表(应答是json串)或字符串(应答是html内容)
        self.request_headers = None  # type: dict
        self.request_body = None  # type: dict
        self.response_status_code = None  # type: str
        # 请求响应耗时 ms
        self.elapsed_time = 0

    def send(self) -> requests.Response:
        # send request
        _start_clock = time.time()
        if self._method == 'get':
            self.response = requests.get(url=self.url, **self.kwargs)
        if self._method == 'post':
            self.response = requests.post(url=self.url, **self.kwargs)
        if self._method == 'delete':
            self.response = requests.delete(url=self.url, **self.kwargs)
        if self._method == 'put':
            self.response = requests.put(url=self.url, **self.kwargs)
        if self._method == 'patch':
            self.response = requests.patch(url=self.url, **self.kwargs)
        if self._method == 'options':
            self.response = requests.options(url=self.url, **self.kwargs)
        if self._method == 'head':
            self.response = requests.head(url=self.url, **self.kwargs)
        # recv response
        _end_clock = time.time()
        self.elapsed_time = int((_end_clock - _start_clock) * 1000) + 1
        self._handle_response()
        return self.response

    def _handle_response(self):
        """处理应答解析拿到 应答体 应答头 请求体 请求头"""
        self._handle_response_status_code()
        self._handle_response_headers()
        self._handle_request_body()
        self._handle_request_headers()
        self._handle_response_body()

    def _handle_response_status_code(self):
        self.response_status_code = str(self.response.status_code)

    def _handle_response_body(self):
        """处理应答体"""
        # self.response_body = self.response.content.decode(self.response.apparent_encoding)
        try:
            if self.response.content:
                file_name = self._check_response_header_has_attachment()
                if file_name:  # 响应头标识为附件
                    self.response_body = str(self.response.content)[2:-1]  # "b'PK\x03\x04....\x14\x00'" -> "PK\x03\x04....\x14\x00"
                    save_binary_to_file(bytes=self.response.content, file_name=file_name)
                else:
                    self.response_body = json.loads(self.response.content)
            else:
                self.response_body = ''
        except json.JSONDecodeError:
            # www.baidu.com 避免应答乱码
            # try:
            #     self.response_body = self.response.content.decode(self.response.encoding)
            # except (UnicodeDecodeError, TypeError):
            #     self.response_body = self.response.content.decode(self.response.apparent_encoding)
            try:
                self.response_body = self.response.content.decode(self.response.apparent_encoding)
            except (UnicodeDecodeError, TypeError):
                self.response_body = self.response.content.decode(self.response.encoding)

    def _handle_response_headers(self):
        """处理应答头"""
        response_headers = copy.deepcopy(self.response.headers)  # dict()方法是不是会重新创建一个新的对象 ,实际为浅复制
        response_headers['status_code'] = self.response_status_code
        response_headers['elapsed_time'] = self.elapsed_time
        self.response_headers = dict(response_headers)  # CaseInsensitiveDict转为Dict

    def _handle_request_body(self):
        """处理请求体"""
        self.request_body = {
            'status_code': self.response_status_code,
            'method': self.response.request.method,
            'url': self.response.url,
            'data': str(self.response.request.body),  # 文件上传请求response.request.body类型为bytes,需要转化为str
        }

    def _handle_request_headers(self):
        """处理请求头"""
        request_headers = copy.deepcopy(self.response.request.headers)
        self.request_headers = dict(request_headers)  # CaseInsensitiveDict转为Dict

    def _check_response_header_has_attachment(self):
        """确认应答头中是否包含附件信息, 如果有则直接返回文件名"""
        check_compile = re.compile('.*(attachment);.*filename=(.+)')
        for key, value in self.response_headers.items():
            if key.lower() == 'content-disposition':
                match = check_compile.match(value)
                if match is not None:
                    file_name = match.group(2)
                    return file_name
        return False


class Get(HTTPRequest):
    _method = 'get'


class Post(HTTPRequest):
    _method = 'post'


class Delete(HTTPRequest):
    _method = 'delete'


class Put(HTTPRequest):
    _method = 'put'


class Patch(HTTPRequest):
    _method = 'patch'


class Options(HTTPRequest):
    _method = 'options'


class Head(HTTPRequest):
    _method = 'head'


def make_request(method, url, *, parameters=None, content_type=None, message_body=None, file_upload=None, **kwargs):
    """
    :param method: 方法 GET/POST...
    :type method: str
    :param url: url地址 http://127.0.0.1:5000
    :type url: str
    :param parameters: 参数数据
    :type parameters: HTTPCaseParameter
    :param content_type: 实际发送的数据类型
    :type content_type: str
    :param message_body: 消息体数据
    :type message_body: str
    :param file_upload: 文件上传参数数据
    :type file_upload: HTTPCaseFileUpload
    :param kwargs: 其他参数
    :type kwargs: dict
    :param headers: 请求头数据
    :type headers: dict
    :rtype: HTTPRequest
    """

    kwargs.pop('url', None)
    headers = kwargs.pop('headers', {})
    files = kwargs.pop('files', None)
    _json = kwargs.pop('json', None)
    _data = kwargs.pop('data', None)
    kwargs.pop('params', None)

    # 选择适当的参数数据, 如果parameters没有数据，则取message_body作为入参数据。（参数和消息体有且只有一个生效）
    if (parameters is not None) and (len(parameters) > 0):
        _data = handle_data_from_params(parameters)
    elif (message_body is not None) and (message_body.strip() != ''):
        try:
            _json = handle_data_form_json_str(json_str=message_body)
        except json.decoder.JSONDecodeError:
            _data = message_body

    # 如果类型错误是否编译通过? 使用rtype即使类型错误也可以编译通过。但如果使用type hints如果类型标识错误则编译不通过
    if method.lower() == 'get':
        params = handle_params(parameters)
        return Get(url=url, params=params, data=None, json=None, headers=headers, files=None, **kwargs)
    if method.lower() == 'post':
        # TODO 如果勾选了对"post使用multipart/form-data"而参数数据为空, 消息体数据有json字符串, 则请求头的content-type会置为application/json
        # files = handle_file_upload(file_upload) if file_upload else None
        if content_type == CONTENT_TYPE.FORM_DATA:
            _json = None
            if parameters:
                _data = handle_data_for_content_type(parameters, content_type=CONTENT_TYPE.FORM_DATA)
                # TODO 支持文件上传可使用下列方法
                # _data.update({'file': ('filename', open(r'filepath', 'rb'), 'text/plain')})
                _data = MultipartEncoder(_data)
                if 'Content-Type' not in headers:
                    headers['Content-Type'] = _data.content_type
        return Post(url=url, params=None, data=_data, json=_json, headers=headers, files=files, **kwargs)
    if method.lower() == 'delete':
        return Delete(url=url, params=None, data=_data, json=_json, headers=headers, files=None, **kwargs)
    if method.lower() == 'put':
        return Put(url=url, params=None, data=_data, json=_json, headers=headers, files=None, **kwargs)
    if method.lower() == 'patch':
        return Patch(url=url, params=None, data=_data, json=_json, headers=headers, files=None, **kwargs)
    if method.lower() == 'options':
        return Options(url=url, params=None, data=_data, json=_json, headers=headers, files=None, **kwargs)
    if method.lower() == 'head':
        return Head(url=url, params=None, data=_data, json=_json, headers=headers, files=None, **kwargs)


if __name__ == '__main__':
    pass
